﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace SocialKit.LightRest.OAuth
{
    /// <summary>
    /// Represents an OAuth http request message.
    /// </summary>
    public class OAuthHttpRequestMessage : HttpRequestMessage
    {
        bool signed = false;

        /// <summary>
        /// Gets the content creator.
        /// </summary>
        public IHttpContentCreator ContentCreator { get; private set; }


        /// <summary>
        /// Initialize a new instance of SocialKit.LightOAuth.OAuthHttpRequestMessage class.
        /// </summary>
        public OAuthHttpRequestMessage() : base() { }

        /// <summary>
        /// Initialize a new instance of SocialKit.LightOAuth.OAuthHttpRequestMessage class.
        /// </summary>
        /// <param name="method">The http method.</param>
        /// <param name="uri">The URI to request.</param>
        public OAuthHttpRequestMessage(string method, Uri uri)
            : base(method, uri) { }

        /// <summary>
        /// Initialize a new instance of SocialKit.LightOAuth.OAuthHttpRequestMessage class.
        /// </summary>
        /// <param name="method">The http method.</param>
        /// <param name="uri">The URI to request.</param>
        /// <param name="creator">The request content creator.</param>
        public OAuthHttpRequestMessage(string method, Uri uri, IHttpContentCreator creator)
            : base(method, uri, creator)
        {
            this.ContentCreator = creator;
        }

        /// <summary>
        /// Initialize a new instance of SocialKit.LightOAuth.OAuthHttpRequestMessage class.
        /// </summary>
        /// <param name="method">The http method.</param>
        /// <param name="uri">The URI string to request.</param>
        public OAuthHttpRequestMessage(string method, string uri)
            : base(method, uri) { }

        /// <summary>
        /// Initialize a new instance of SocialKit.LightOAuth.OAuthHttpRequestMessage class.
        /// </summary>
        /// <param name="method">The http method.</param>
        /// <param name="uri">The URI string to request.</param>
        /// <param name="creator">The request content creator.</param>
        public OAuthHttpRequestMessage(string method, string uri, IHttpContentCreator creator)
            : base(method, uri, creator)
        {
            this.ContentCreator = creator;
        }


        /// <summary>
        /// Sets the request content with the creator then returns this request itself.
        /// </summary>
        /// <param name="creator">The request content creator.</param>
        /// <returns></returns>
        public override HttpRequestMessage SetContent(IHttpContentCreator creator)
        {
            this.ContentCreator = creator;

            return base.SetContent(creator);
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="accessToken">The access token to sign request.</param>
        /// <param name="additionalParameters">The additional parameters normalized as URI query string(like a=1&amp;b=2).</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(AccessToken accessToken, string additionalParameters)
        {
            return Sign(accessToken.Consumer, accessToken.Token, accessToken.Secret, RestUtility.ParseQueryString(additionalParameters).ToArray());
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="accessToken">The access token to sign request.</param>
        /// <param name="additionalParameters">The additional parameters to sign.</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(AccessToken accessToken, params KeyValuePair<string, string>[] additionalParameters)
        {
            return Sign(accessToken.Consumer, accessToken.Token, accessToken.Secret, additionalParameters);
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="consumer">The OAuth consumer instance.</param>
        /// <param name="token">The token.</param>
        /// <param name="tokenSecret">The secret of token.</param>
        /// <param name="additionalParameters">The additional parameters normalized as URI query string(like a=1&amp;b=2).</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Consumer consumer, string token, string tokenSecret, string additionalParameters)
        {
            return Sign(null, consumer, token, tokenSecret, RestUtility.ParseQueryString(additionalParameters).ToArray());
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="consumer">The OAuth consumer instance.</param>
        /// <param name="token">The token.</param>
        /// <param name="tokenSecret">The secret of token.</param>
        /// <param name="additionalParameters">The additional parameters to sign.</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Consumer consumer, string token, string tokenSecret, params KeyValuePair<string, string>[] additionalParameters)
        {
            return Sign(null, consumer, token, tokenSecret, additionalParameters);
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="baseAddress">The base host URI of service provider, if this request has a relative URI, the base address URI is required.</param>
        /// <param name="accessToken">The access token to sign request.</param>
        /// <param name="additionalParameters">The additional parameters normalized as URI query string(like a=1&amp;b=2).</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Uri baseAddress, AccessToken accessToken, string additionalParameters)
        {
            return Sign(baseAddress, accessToken, RestUtility.ParseQueryString(additionalParameters).ToArray());
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="baseAddress">The base host URI of service provider, if this request has a relative URI, the base address URI is required.</param>
        /// <param name="accessToken">The access token to sign request.</param>
        /// <param name="additionalParameters">The additional parameters to sign.</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Uri baseAddress, AccessToken accessToken, params KeyValuePair<string, string>[] additionalParameters)
        {
            return Sign(baseAddress, accessToken.Consumer, accessToken.Token, accessToken.Secret, additionalParameters);
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="baseAddress">The base host URI of service provider, if this request has a relative URI, the base address URI is required.</param>
        /// <param name="consumer">The OAuth consumer instance.</param>
        /// <param name="token">The token.</param>
        /// <param name="tokenSecret">The secret of token.</param>
        /// <param name="additionalParameters">The additional parameters normalized as URI query string(like a=1&amp;b=2).</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Uri baseAddress, Consumer consumer, string token, string tokenSecret, string additionalParameters)
        {
            return Sign(baseAddress, consumer, token, tokenSecret, RestUtility.ParseQueryString(additionalParameters).ToArray());
        }

        /// <summary>
        /// Sign this request with OAuth.
        /// </summary>
        /// <param name="baseAddress">The base host URI of service provider, if this request has a relative URI, the base address URI is required.</param>
        /// <param name="consumer">The OAuth consumer instance.</param>
        /// <param name="token">The token.</param>
        /// <param name="tokenSecret">The secret of token.</param>
        /// <param name="additionalParameters">The additional parameters to sign.</param>
        /// <returns></returns>
        public OAuthHttpRequestMessage Sign(Uri baseAddress, Consumer consumer, string token, string tokenSecret, params KeyValuePair<string, string>[] additionalParameters)
        {
            if (signed)
                throw new InvalidOperationException("This request message has already signed.");

            if (baseAddress == null && !this.Uri.IsAbsoluteUri)
                throw new ArgumentNullException("baseAddress", "This request message has a relative URI and need an absolute URI of service provider to sign.");

            if (baseAddress != null && !baseAddress.IsAbsoluteUri && !this.Uri.IsAbsoluteUri)
                throw new ArgumentOutOfRangeException("baseAddress", "The baseAddress must an absolute URI when this request message has a relative URI.");

            if (!this.Uri.IsAbsoluteUri)
                this.Uri = new Uri(baseAddress, this.Uri);

            //Attach oauth parameters to the query string
            var query = new HttpQueryString(additionalParameters)
                .Append(OAuthStrings.OAuthConsumerKeyKey, consumer.Key)
                .Append(OAuthStrings.OAuthSignatureMethodKey, consumer.SignatureProvider.SignatureMethod)
                .Append(OAuthStrings.OAuthNonceKey, OAuthUtil.GenerateNonce())
                .Append(OAuthStrings.OAuthTimestampKey, OAuthUtil.GenerateTimestamp())
                .Append(OAuthStrings.OAuthVersionKey, Consumer.OAuthVersion);

            if (!string.IsNullOrEmpty(token))
                query.Add(OAuthStrings.OAuthTokenKey, token);

            this.Uri = query.MakeQueryString(this.Uri);


            var signature = consumer.SignatureProvider.ComputeSignature(this.SignatureBaseString, consumer.Secret, tokenSecret);

            this.Uri = new HttpQueryString()
                .Append(OAuthStrings.OAuthSignatureKey, signature)
                .MakeQueryString(this.Uri);


            signed = true;

            return this;
        }

        /// <summary>
        /// Gets the base string for OAuth signature of this request message.
        /// </summary>
        /// <returns></returns>
        public string SignatureBaseString
        {
            get
            {
                if (!this.Uri.IsAbsoluteUri)
                    throw new NotSupportedException("The signature base string required an absolute URI, please specify the base URI when calling the Sign method.");

                var valuesToSign = RestUtility.ParseQueryString(this.Uri.Query)
                    .Concat(FetchValuesFromContent(this.ContentCreator));

                var normalizedQuery = string.Join("&",
                    valuesToSign.OrderBy(p => p.Key)
                    .Select(p => string.Format("{0}={1}",
                        RestUtility.UrlEncode(p.Key),
                        RestUtility.UrlEncode(p.Value))
                        ).ToArray());

                return string.Join("&", new string[] { 
                    this.Method.ToUpper(), 
                    RestUtility.UrlEncode(RestUtility.NormalizeUriWithoutQuery(this.Uri)), 
                    RestUtility.UrlEncode(normalizedQuery)
                });
            }
        }

        static IEnumerable<KeyValuePair<string, string>> FetchValuesFromContent(IHttpContentCreator creator)
        {
            if (creator is HttpUrlEncodedForm)
                return (creator as HttpUrlEncodedForm);
            else if (creator is HttpMultipartMimeForm)
                return (creator as HttpMultipartMimeForm).Values;
            else
                return new KeyValuePair<string, string>[] { };
        }
    }
}